fix(linear): converge snapshot cache reader/writer keys (TAP-4588)#201
Conversation
The Linear snapshot payload cache almost never self-hit: the reader (tapps_linear_snapshot_get) and the writer (auto-populate hook + agents' own get/put) computed different keys for the same open-issue slice. The sentinel/gate layer already collapsed aliases (TAP-1374) but the payload key did not. Root cause + fix: - state alias: add _canonical_state() mapping ""/None/"open"/open-bucket members to one canonical "open" token; _resolve_cache_key canonicalizes the filename segment AND the hashed state field. Closed buckets (completed/canceled) stay isolated. Mirrored in the two bash key snippets, the PowerShell variant, and all 3 regenerated committed hooks. - limit in hash: dropped from the key entirely; enforced at read time via a superset-limit fallback in snapshot_get (stored>=requested serves, truncated, served_from_superset=true; smaller stored limit MISSES). snapshot_put now records the stored limit. - poisoning: snapshot_get returns a miss (not a false empty hit) for an auto_populated payload with issues==[]; the auto-populate hook skips the write when the request state was an alias/invalid AND the result is empty. Tests: +7 behavioral tests (canonicalization hit, superset truncation, smaller-can't-serve-larger, closed-bucket isolation, poisoning guard, manual-empty-still-hits). Updated the assertions that encoded the pre-fix multi-key/limit-sensitive behavior to the new convergence contract (TAP-1374 cross-alias unlock preserved via convergence). Full tapps-mcp unit suite green (5952 passed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 89dfc85abe
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if state_lc == "" or state_lc == _CANONICAL_OPEN_STATE or state_lc in _OPEN_STATE_BUCKETS: | ||
| return _CANONICAL_OPEN_STATE |
There was a problem hiding this comment.
Do not collapse concrete Linear states into one cache key
When a cache entry is populated from a concrete list_issues call such as state="started", this canonicalization makes snapshot_get(state="backlog"), snapshot_get(state="unstarted"), and snapshot_get(state="open") all read the same file. The existing linear-read flow explicitly fetches and stores using the same concrete state as the requested list_issues filter (.claude/skills/linear-read/SKILL.md lines 16-21), so a narrow started-only payload can be served as a backlog result without another Linear fetch, causing agents to miss or act on the wrong issues. Keep the broad open alias separate from concrete state-filtered snapshots, or only store under open when the payload actually contains the full open-state superset.
Useful? React with 👍 / 👎.
Summary
The Linear snapshot payload cache (
.tapps-mcp-cache/linear-snapshots/<key>.json) almost never self-hit: the reader (tapps_linear_snapshot_get) and the writer (auto-populate hook + agents' own get/put) computed different keys for the same open-issue slice. The sentinel/gate layer already collapsed aliases (TAP-1374) but the payload key did not.Fixes TAP-4588.
Root cause + fix
get(state="open")used the tapps-mcp TTL alias, not a Linear state; real reads usestarted/unstarted/backlog/"". Added_canonical_state()mapping""/None/open/open-bucket members to one canonical"open"token;_resolve_cache_keycanonicalizes both the filename segment and the hashedstatefield. Closed buckets (completed/canceled) stay isolated. Mirrored inLINEAR_CACHE_GATE_KEY_PY,LINEAR_CACHE_GATE_POST_LIST_SCRIPT, the PowerShellLINEAR_CACHE_GATE_KEY_PS, and all 3 regenerated committed bash hooks.limitin the hash.get(limit=150)hashed differently from the writer defaultlimit=50. Droppedlimitfrom the key; enforced at read time via a superset-limit fallback insnapshot_get(storedlimit >= requestedserves, truncated,served_from_superset=true; a smaller stored limit still MISSES).snapshot_putrecords the stored limit.list_issues(state="open")returns[](invalid Linear state) and the hook cached it. Nowsnapshot_getreturns a miss for anauto_populatedpayload withissues==[], and the auto-populate hook skips the write when the request state was an alias/invalid AND the result is empty. A manual empty put for a genuinely-empty slice still hits.Tests
TestAliasUnlock).×-in-docstring warnings on master left untouched).state="started"→ serverget(state="open")HITS;list_issues(state="open")→[]does not poison the good snapshot.🤖 Generated with Claude Code